Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forward auth #6

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open

Forward auth #6

wants to merge 12 commits into from

Conversation

traverseda
Copy link
Contributor

Support for traefik forward auth, which lets us apply basic yes/no authentication to web pages.

@traverseda
Copy link
Contributor Author

A bit of an easier start then OIDC. Forward auth is pretty simple, the view returns 2xx is the user is allowed to view that url, 403 Forbidden if they're not, and 401 Unauthorized if they're not logged in yet.

The proxy (traefik or nginx or whatever) interprets that response and decides what to do with it. If the response isn't 200 then it shows the content of the response instead of the app.

Session management is a bit trickier, but hopefully not too bad. I've got a busy work week so I won't be able to do much with this, but I figured I'd commit these stubs.

Expect it to be a few weeks for these two features to get completed given my schedule.

@sonicnkt
Copy link
Owner

sonicnkt commented Apr 8, 2021

This sounds intresting and i would definatly have some use for it to secure some static sites where i am currently only using basic http auth.

You also mentioned nginx which i am using at home and at work. Is this what you are talking about:
https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/

In this config example the requested uri is forwarded to the authentication provider. So we could add checks in flask based on which url is forwarded and grant accses based group membership?
Then we could add another table which just links the URLs to specific groups or users.

Im quite busy at work atm as well so no rush :) i plan to work on the app a bit but only some ui improvements and polishment. I havent done much with html/css in a while...

@traverseda
Copy link
Contributor Author

Then we could add another table which just links the URLs to specific groups or users.

Yep, that's the idea. I'm not sure how sessions work in that whole scheme, whether it sets a cookie as domain it's authenticating or what. It works pretty much the same in traefik as it does in nginx.

@traverseda
Copy link
Contributor Author

As an aside with these two features this project has pretty much the same features as a slapd+keycloak+authelia setup, except it's much easier to deploy and doesn't take ~600mb+ of ram.

@traverseda
Copy link
Contributor Author

Session cookies are definitely being a pain, there are two ways to do it and one of them is going to require a real domain name instead of localhost. So I guess we're going to be setting cookies via javascript, which isn't my favorite but I don't see any other way to do this. I think it's doable via javascript, but it's definitely going to be a pain.

Basically we need a way to set a cookie on whatever domain traefik is in front of, and we can't pass any data to the proxied login form.

This gets around the cross-domain cookie tracking protection, and works with pseudo-domains like localhost. Feels like it's a bit of a broken spec though, and that this was a lot easier in the past.

@traverseda
Copy link
Contributor Author

Worked on a chunk of the javacript stuff, a lot of getting the CORS headers to work cleanly. Still having issues with setting the cookies though, think I'm going to need to send the cookie out of band, then sit back and think about the security implications of the abomination I've created.

@sonicnkt
Copy link
Owner

hey, just stubmled upon this project.
https://github.com/mbreese/subauth
Maybe it could help with this?

@traverseda traverseda marked this pull request as ready for review May 1, 2021 18:16
@traverseda
Copy link
Contributor Author

HTTP basic auth was always on my list, I wanted to get cross-domain cookie auth working first though. Unfortunately that's being a pain, and I worry that cookie policies in the future will make that even worse.

For cookie/html based login the big thing we're missing is a way to set the cookie on the origin domain, and have it actually stick. I've put a fair bit of work in on making glauth-ui respect arbitrary domains and the like (much more annoying than just subdomains), and be able to add domain-specific cookies. I figure we should keep that code in, as it is a feature that we probably want eventually as it allows for actually embedding the login page in other sites.

But for now HTTP basic auth does get the job done.

@sonicnkt
Copy link
Owner

sonicnkt commented May 1, 2021

Thanks for all the work you put into this.
So if i understand you correctly authentication works, tho not with our native login form but the basic http auth dialog? If a user is allready logged in tho it will just redirect to the page straight away?
Native login would be cool but its a great start.

Shouldnt it be possible to redirect to the the native login form keeping the original url you wanted to open in the arguments and then after this just do a new a request to access the desired page so this time it sees the user is authenticated and respond with 201 straight away?

I have also seen that you can set whitelisted ips or the required group membership using arguments in the request header. So you set those in your webserver/proxy configuration outside of glauth-ui.

A nice addition would be to also support adress blocks with a netmask (192.168.1.0/24) to check against. So you could whiteliste a whole ip range easily (for company or home networks).

Can this be exploited by setting those somehow manually? (i have no experience with this, just wondering) or is this all handled internally and never exposed to the enduser?

@traverseda
Copy link
Contributor Author

Isnt it somehow possible to redirect to a login form and then return the response to the proxy or is this only possible with cookies

Unfortunately it's not. The CORS cookie stuff is complicated and I don't entirely understand it, but basically you can only set the cookie for the site you currently are. Unfortunately under traefik what happens is you do a post request to proxied.site/login and... it redirects you back to the base site.

You need to do all your login stuff over javascript. I tried to sort of avoid that using the lovely htmx library, got the headers sorted out so it could in theory take the cookie from the htmx AJAX request and set it, and firefox just refuses to actually do that.

So I don't know. The solution to that would involve creating a new login flow that works entirely via javscript, and sets the cookie based on some kind of json response, which I find unpleasant.

So yeah, I tried to redirect to the native login form and the first several approaches I took didn't work. I find the remaining approach generally unpleasent, but I'll probably get around to it eventually.

Can this be exploited by setting those somehow manually?

That endpoint is set by the reverse proxy, in the reverse proxy config file, so it never gets expose to the user.

A nice addition would be to also support adress blocks with a netmask

That would be a nice addition. I just threw the ip filter in because it was fast.

@sonicnkt
Copy link
Owner

sonicnkt commented May 1, 2021

sorry i edited my post a bunch of times so you probably missed my latest addition:

Shouldnt it be possible to redirect to the the native login form keeping the original url you wanted to open in the arguments and then after this just do a redirect to the original page you wanted to acces so this time it sees the user is authenticated and respond with 201 straight away?

That would be a nice addition. I just threw the ip filter in because it was fast.

After i tested this with nginx and merged your pull request i will add it, i bet there is a small python library for checking this arround.

@traverseda
Copy link
Contributor Author

@sonicnkt Just hopefully added support for network based whitelisting. It didn't break anything, but I haven't tested it.

@sonicnkt
Copy link
Owner

sonicnkt commented May 3, 2021

@traverseda i just tried to run your fork but it wont work without SERVER_NAME being set:

class DomainSessionInterface(flask.sessions.SecureCookieSessionInterface):
    def get_cookie_domain(self, app):
        origin=request.headers.get('Origin',Config.SERVER_NAME).split("://")[-1]
        if origin=="localhost":
            origin=""
        return origin

    def get_cookie_httponly(self, app):
        return False

Is this necessary for the simple forward auth to work correctly? i removed it from my current version again as it didnt seem to be necessary for anything.
I also noticed a few other snippets that are not used at this moment. Maybe we should leave this out until its functional so we keep the main code as simple as possible for now.

@traverseda
Copy link
Contributor Author

traverseda commented May 3, 2021

It's necessary to have domain-based routing working, which is not necessary for http basic auth, but would be necessary for proxied-web-form/cookie auth.

I had considered going back and taking just the changes necessary to do basic header auth, I think that would have been cleaner. That would basically just be the model method for checking is a user is in a group, the forward_auth/header/ route, and the thing that logs the user in based on auth header. The route would need to be modified to not take a domain.

But honestly I just didn't have enough time this week. I think it would have been cleaner to, at this point, just support the basic forward auth and ignore all the CORS/cookie stuff completely, and I definitely won't be offended if you separate out just those changes. I just didn't have the time right now.

This weekend I can probably do that, if you don't want to.

@sonicnkt
Copy link
Owner

sonicnkt commented May 3, 2021

thanks for the quick reply.

Yes this would be great if you could do this as i probably mess something up while doing it on my own. All this advanced authentication stuff is a bit above me right now.

I will push some others small changes to main repo in the next days and i also want to setup a dev branch in the future to keep things more clean when adding automatic docker building to the main branch.

@traverseda
Copy link
Contributor Author

Alright, quick fix so that it's forward auth only. Still touches a lot of files because whenever I edit one of your files my text editor doesn't like the trailing whitespace and automatically removes it.

@sonicnkt
Copy link
Owner

sonicnkt commented May 4, 2021

thanks for the cleanup... i will try this as soon as possible then merge it into the main branch.

@sonicnkt
Copy link
Owner

sonicnkt commented May 4, 2021

just had some time to play around with this and after figuring out how to configure nginx i got it to work. Had some issues tho.

  1. in models.py you are using the function authstr.removeprefix("Basic ") and this was missing for me. Maybe its not available in python3.8? Replacing it with authstr.replace("Basic ", "")` fixed this.

  2. Authentication works then but if you enter a wrong username/password it crashes as it cant handle this atm it seems:

  File "/home/nils/python/glauth-ui-dev/app/models.py", line 143, in http_basic_auth
    if user.check_password(password):
AttributeError: 'NoneType' object has no attribute 'check_password'

It also wont ask again for other credentials so you are kind of stuck in this state.

  1. At the moment in only works if you supply a group name, i think tho this should be optional and that if you dont supply a group it should just allow any user that has authenticated.

  2. The Response if the user is anonymous wont be displayed for me (neither firefox nor chromium) The only thing displayed in the dialog is the "Login Required" (Basic realm).


In my setup im hosting glauth-ui on my domain "my.example.com" and want to use it to control access to specific subfolders that store mostly static content ("my.example.com/download", "my.example.com/wiki").
With this setup it should be possible to use a native flask login page by catching the 401 error in nginx and redirecting the user to the login page. Then after sucessfull login redirect to the original URI.
This should work without any additional cookie stuff but of course wont work across domains.

I will try to test this in the next days and see if i can get it working.

@traverseda
Copy link
Contributor Author

    if user.check_password(password):
        AttributeError: 'NoneType' object has no attribute 'check_password'

Ahh, I didn't check using completely non-existent users. Should be easy enough to fix.

Not re-asking for credentials is definitely a problem. I think I can clean that up a bit better. Also looking at adding support for the REMOTE_USER header, which is not great security-wise but some programs support it.

@sonicnkt
Copy link
Owner

sonicnkt commented May 4, 2021

Your current response message if the user is anonymous wont be displayed for me either (on firefox and chromium) only the basic realm is displayed (Login Required) in the login dialog. Is this working for you?

@traverseda
Copy link
Contributor Author

It is not, I'm not sure there's any real way to set a message other than editing the realm.

Honestly I spent most of my time trying to get flask working with multiple domains, and multiple cookie domains. This hasn't been tested thoroughly, really appreciate the testing.

@sonicnkt
Copy link
Owner

sonicnkt commented May 4, 2021

Got this fully working with nginx (yay :) ):

  location / {
        proxy_pass http://192.168.1.41:5000/;
        include     common_proxy_location.conf;
        }

  location /wiki/ {
        alias /etc/nginx/test/;
        autoindex on;
        auth_request /auth;
        set $auth_request_args "?group=wiki";
        }

  location /private/ {
        alias /etc/nginx/test/;
        autoindex on;
        auth_request /auth;
        set $auth_request_args "?group=private";
        }

  location /auth {
        internal;
        proxy_pass http://192.168.1.41:5000/forward_auth/header/$auth_request_args;
        proxy_pass_request_body off;
        proxy_set_header        Content-Length "";
        proxy_set_header        Host $http_host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-URI $request_uri;
        proxy_set_header X-Forwarded-Host $http_host;
        }
  error_page 401 = @error401;

  location @error401 {
        return 302 /login?next=$request_uri;
        }

Works great if everything is on the same domain and you want to secure mutliple sublocations to different groups... maybe i will add a custom login page which displays the URI you will be redirected to.

I guess we should setup a wiki sooner or later with config/setup examples...

@sonicnkt
Copy link
Owner

sonicnkt commented May 4, 2021

Here is my code modification for allowing every authenticated user if no group(s) was supplied:

    allowed_groups = request.args.getlist('group')
    if current_user.is_authenticated:
        if allowed_groups:
            if current_user.in_groups(*allowed_groups):
                return "", 201
        else:    
            return "", 201

@traverseda
Copy link
Contributor Author

Sorry about this, haven't had much time to do this sort of thing for a while. Hoping I'll have some time in the near future.

@sonicnkt
Copy link
Owner

No problem :) im only starting to get into this myself again. It just worked for my purpose and i did not have the time either to work on this sort of thing. As this grew longer and longer the hurdle to get into flask grew as well...

Anway the basic functionality is integrated just lacks documentation and some examples, will add this to my todo and after other things are implemented i would like to revisit this again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants